package net.mkserkan.util;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import jxl.Workbook;
import jxl.format.Colour;
import jxl.write.Label;
import jxl.write.WritableCellFormat;
import jxl.write.WritableSheet;
import jxl.write.WritableWorkbook;
import jxl.write.WriteException;

public class Text2Excel implements FileFilter {
	
	private static final Logger LOGGER = Logger.getLogger(Text2Excel.class.getName());
	
	private static final String T2E_SETTINGS_FILE_JVM_PARAM = System.getProperty("t2e.settings.file");
	private static final String SETTINGS_FILE_NAME = T2E_SETTINGS_FILE_JVM_PARAM == null ? "text2excel.properties" : T2E_SETTINGS_FILE_JVM_PARAM;
	private static final Pattern INDEXED_KEYS_PATTERN = Pattern.compile("(\\w+)_(\\d+)");
	private static final Pattern EXCLUSION_PATTERN = Pattern.compile("([^\\;]+)\\;?");
	private static final Pattern COLORS_PATTERN = Pattern.compile("([^\\:]+)\\:([^\\;]+)\\;?");
	
	private static final String PROP_KEY_CHARSET = "Charset";
	private static final String PROP_KEY_FIRST_REPLACE_FROM = "FirstReplaceFrom";
	private static final String PROP_KEY_FIRST_REPLACE_TO = "FirstReplaceTo";
	private static final String PROP_KEY_FIRST_REPLACE_MULTILINE = "FirstReplaceMultiline";
	private static final String PROP_KEY_PATTERN = "Pattern";
	private static final String PROP_KEY_PATTERN_MULTILINE = "PatternMultiline";
	private static final String PROP_KEY_COLORING_MIXED = "ColoringMixed";
	private static final String PROP_KEY_FORCE_SINGLE_LINE = "ForceSingleLine";
	private static final String PROP_KEY_COLORING = "Coloring";
	private static final String PROP_KEY_EXCLUSION = "Exclusion";
	private static final String PROP_KEY_TEXT_FILE_EXTENSIONS = "TextFileExts";
	private static final String PROP_KEY_SHEET_NAME_PATTERN = "SheetNamePattern";
	
	private Charset charset = Charset.forName("UTF-8");
	
	private Pattern firstReplacePattern;
	private String firstReplaceString;
	
	private Pattern pattern;
	
	private Map<Integer, Boolean> forceSingleLine = new HashMap<Integer, Boolean>();
	private Map<Integer, List<Pattern>> exclusions = new HashMap<Integer, List<Pattern>>();
	
	private Map<Integer, Map<Pattern, Colour>> colors = new HashMap<Integer, Map<Pattern, Colour>>();
	private Colour colourMixed = Colour.GREY_25_PERCENT;
	
	private List<String> textFileExtensions = new ArrayList<String>();
	
	private Pattern sheetNamePattern;
	
	public Text2Excel() throws IOException, PatternSyntaxException {
		Properties properties = new Properties();
		loadProperties(properties);
		
		String charsetStr = (String) properties.get(PROP_KEY_CHARSET);
		if (charsetStr != null) {
			try {
				charset = Charset.forName(charsetStr);
			}
			catch (UnsupportedCharsetException e) {
				LOGGER.warning("\"" + PROP_KEY_CHARSET + "\" specified in the properties not valid : " + charsetStr);
			}
		}
		
		String textFileExtensionsStr = (String) properties.get(PROP_KEY_TEXT_FILE_EXTENSIONS);
		if (textFileExtensionsStr != null) {
			String[] exts = textFileExtensionsStr.split(",");
			textFileExtensions.addAll(Arrays.asList(exts));
		}
		
		String sheetNamePatternStr = (String) properties.get(PROP_KEY_SHEET_NAME_PATTERN);
		if (sheetNamePatternStr != null) {
			sheetNamePattern = Pattern.compile(sheetNamePatternStr);
		}
		
		String firstReplaceFromStr = (String) properties.get(PROP_KEY_FIRST_REPLACE_FROM);
		if (firstReplaceFromStr != null) {
			String firstReplaceMultilineStr = (String) properties.get(PROP_KEY_FIRST_REPLACE_MULTILINE);
			boolean firstReplaceMultiline = "true".equals(firstReplaceMultilineStr);
			firstReplacePattern = Pattern.compile(firstReplaceFromStr, firstReplaceMultiline ? Pattern.MULTILINE : 0);
			
			String firstReplaceToStr = (String) properties.get(PROP_KEY_FIRST_REPLACE_TO);
			firstReplaceString = firstReplaceToStr == null ? "" : firstReplaceToStr;
		}
		
		String patternStr = (String) properties.get(PROP_KEY_PATTERN);
		if (patternStr != null) {
			String patternMultilineStr = (String) properties.get(PROP_KEY_PATTERN_MULTILINE);
			boolean patternMultiline = "true".equals(patternMultilineStr);
			pattern = Pattern.compile(patternStr, patternMultiline ? Pattern.MULTILINE : 0);
		}
		else
			throw new IOException("\"" + PROP_KEY_PATTERN + "\" not found!");
		
		String coloringMixedStr = (String) properties.get(PROP_KEY_COLORING_MIXED);
		if (coloringMixedStr != null) {
			Colour colourMixed = getColour(coloringMixedStr);
			if (colourMixed == null)
				LOGGER.warning("\"" + PROP_KEY_COLORING_MIXED
						+ "\" specified in the properties contains invalid colour name : " + coloringMixedStr);
			else
				this.colourMixed = colourMixed;
		}
		
		Map<Integer, String> forceSingleLineValues = getIndexedKeys(PROP_KEY_FORCE_SINGLE_LINE, properties);
		for (Map.Entry<Integer, String> entry : forceSingleLineValues.entrySet())
			forceSingleLine.put(entry.getKey(), new Boolean(entry.getValue()));
		
		Map<Integer, String> coloringValues = getIndexedKeys(PROP_KEY_COLORING, properties);
		for (Map.Entry<Integer, String> entry : coloringValues.entrySet())
			setColoring(entry.getKey(), entry.getValue());
		
		Map<Integer, String> exclusionValues = getIndexedKeys(PROP_KEY_EXCLUSION, properties);
		for (Map.Entry<Integer, String> entry : exclusionValues.entrySet())
			setExclusions(entry.getKey(), entry.getValue());
	}
	
	private Map<Integer, String> getIndexedKeys(String prefix, Properties properties) {
		Map<Integer, String> result = new HashMap<Integer, String>();
		Set<String> keySet = properties.stringPropertyNames();
		for (String key : keySet) {
			Matcher matcher = INDEXED_KEYS_PATTERN.matcher(key);
			if (matcher.matches()) {
				String g1 = matcher.group(1);
				if (g1.equalsIgnoreCase(prefix))
					result.put(new Integer(matcher.group(2)), properties.getProperty(key));
			}
		}
		return result;
	}
	
	private void setExclusions(int i, String exclusionsStr) {
		List<Pattern> exclusions = new ArrayList<Pattern>();
		if (exclusionsStr != null) {
			exclusions = new ArrayList<Pattern>();
			
			Matcher matcher = EXCLUSION_PATTERN.matcher(exclusionsStr);
			while (matcher.find()) {
				String exclusion = matcher.group(1);
				exclusions.add(Pattern.compile(exclusion, Pattern.CASE_INSENSITIVE));
			}
			if (exclusions.size() == 0)
				LOGGER.warning("\"" + PROP_KEY_EXCLUSION + "_" + i + "\"  specified in the properties not valid.");
			else
				this.exclusions.put(i, exclusions);
		}
	}
	
	private void setColoring(int i, String coloringStr) {
		Map<Pattern, Colour> colours = new HashMap<Pattern, Colour>();
		if (coloringStr != null) {
			Matcher matcher = COLORS_PATTERN.matcher(coloringStr);
			while (matcher.find()) {
				String colourPattern = matcher.group(1);
				String colourName = matcher.group(2);
				
				Colour colour = getColour(colourName);
				if (colour != null)
					colours.put(Pattern.compile(colourPattern, Pattern.CASE_INSENSITIVE), colour);
				else
					LOGGER.warning("\"" + PROP_KEY_COLORING + "_" + i
							+ "\" specified in the properties contains invalid colour name : " + colourName);
			}
			if (colours.size() == 0)
				LOGGER.warning("\"Coloring_" + i + "\" specified in the properties not valid.");
			else
				this.colors.put(new Integer(i), colours);
		}
	}
	
	private Colour getColour(String colourName) {
		Colour[] colours = Colour.getAllColours();
		Colour found = null;
		for (Colour colour : colours) {
			if (colour.getDescription().equalsIgnoreCase(colourName)) {
				found = colour;
				break;
			}
		}
		return found;
	}
	
	public void toExcel(String str, WritableWorkbook workbook, String sheetName, int sheetIndex) throws IOException, WriteException {
		if (sheetNamePattern != null) {
			Matcher sheetNameMatcher = sheetNamePattern.matcher(sheetName);
			if (sheetNameMatcher.find()) {
				int start = sheetNameMatcher.start();
				int end = sheetNameMatcher.end();
				sheetName = sheetName.substring(start, end);
			}
		}
		sheetName = sheetName.length() > 31 ? sheetName.substring(0, 31) : sheetName;
		
		WritableSheet sheet = workbook.createSheet(sheetName, sheetIndex);
		
		if (firstReplacePattern != null) {
			Matcher replaceMatcher = firstReplacePattern.matcher(str);
			str = replaceMatcher.replaceAll(firstReplaceString);
		}
		
		Matcher matcher = pattern.matcher(str);
		int i = 0;
		MAIN: while (matcher.find()) {
			int groupCount = matcher.groupCount();
			for (int j = 1; j <= groupCount; j++) {
				String group = matcher.group(j);
				
				Boolean forceSL = forceSingleLine.get(j);
				if (forceSL != null && forceSL)
					group = group.replaceAll("[\\r\\n]", " ");
				
				List<Pattern> exclusion = exclusions.get(j);
				if (exclusion != null)
					for (Pattern pattern : exclusion) {
						Matcher excMatcher = pattern.matcher(group);
						if (excMatcher.find()) {
							continue MAIN;
						}
					}
				
				WritableCellFormat cellFormat = new WritableCellFormat();
				Map<Pattern, Colour> coloring = colors.get(j);
				if (coloring != null) {
					Colour coloured = null;
					for (Map.Entry<Pattern, Colour> entry : coloring.entrySet()) {
						Pattern colourPattern = entry.getKey();
						Colour colour = entry.getValue();
						Matcher colourMatcher = colourPattern.matcher(group);
						if (colourMatcher.find()) {
							if (coloured != null && !colour.equals(coloured)) {
								cellFormat.setBackground(colourMixed);
								break;
							}
							else {
								cellFormat.setBackground(colour);
								coloured = colour;
							}
						}
					}
				}
				
				Label label = new Label(j - 1, i, group.substring(0, (group.length() > 32767 ? 32767 : group.length())));
				label.setCellFormat(cellFormat);
				sheet.addCell(label);
			}
			
			i++;
		}
	}
	
	public boolean accept(File file) {
		for (String ext : textFileExtensions)
			if (file.getName().endsWith("." + ext))
				return true;
		return file.getName().endsWith(".txt");
	}
	
	protected static String readFromFile(File file, Charset charset) throws IOException {
		InputStream in = new FileInputStream(file);
		byte[] bytes = new byte[(int) file.length()];
		in.read(bytes);
		in.close();
		in = null;
		String str = new String(bytes, charset);
		
		System.gc();
		return str;
	}
	
	protected static void convertFile(File file, Text2Excel text2Excel, WritableWorkbook workbook, int index) throws IOException, WriteException {
		String fileName = file.getName();
		
		LOGGER.info("Converting to Excel : \"" + file.getAbsolutePath() + "\"");
		String str = readFromFile(file, text2Excel.charset);
		LOGGER.info("Text Length = " + str.length());
		text2Excel.toExcel(str, workbook, fileName, index);
	}
	
	protected static void loadProperties(Properties properties) throws IOException {
		InputStream settingsIn;
		if (SETTINGS_FILE_NAME.startsWith("classpath:")) {
			String classpathResource = SETTINGS_FILE_NAME.substring(10);
			settingsIn = Text2Excel.class.getClassLoader().getResourceAsStream(classpathResource);
			if (settingsIn == null)
				throw new IOException(classpathResource + " not found at the classpath!");
		}
		else {
			settingsIn = new FileInputStream(SETTINGS_FILE_NAME);
		}
		properties.load(settingsIn);
		settingsIn.close();
	}
	
	public static void main(String[] args) {
		try {
			Text2Excel text2Excel = new Text2Excel();
			
			List<File> files = new ArrayList<File>();
			for (String arg : args) {
				try {
					File file = new File(arg);
					if (file.exists() && file.isFile()) {
						files.add(file);
					}
					else if (file.exists() && file.isDirectory()) {
						File[] filteredFiles = file.listFiles(text2Excel);
						files.addAll(Arrays.asList(filteredFiles));
					}
					else
						LOGGER.warning("\"" + arg + "\" is not a file.");
				}
				catch (Exception e) {
					LOGGER.warning("\"" + arg + "\" not found.");
				}
			}
			
			WritableWorkbook workbook = null;
			try {
				File xlsFile = new File("fromtext.xls");
				workbook = Workbook.createWorkbook(xlsFile);
				
				for (int i = 0; i < files.size(); i++) {
					convertFile(files.get(i), text2Excel, workbook, i);
				}
				if (workbook.getNumberOfSheets() > 0)
					workbook.write();
			}
			catch (Exception e) {
				LOGGER.log(Level.SEVERE, "Error while converting file", e);
			}
			finally {
				if (workbook != null)
					workbook.close();
			}
		}
		catch (PatternSyntaxException e) {
			LOGGER.log(Level.SEVERE, e.getMessage());
		}
		catch (WriteException e) {
			LOGGER.log(Level.SEVERE, e.getMessage());
		}
		catch (FileNotFoundException e) {
			LOGGER.log(Level.SEVERE, e.getMessage());
		}
		catch (IOException e) {
			LOGGER.log(Level.SEVERE, "IO Error " + e.getMessage());
		}
	}
}
